package seasonEvent

import (
	"gitee.com/kingzyt/common/dbRedis"
	"gitee.com/kingzyt/common/log"
	. "gitee.com/kingzyt/common/util"
	"strings"
	"time"
)

const (
	check_state_normal = 0
	check_state_going  = 1
)

type SeasonEvent struct {
	dbac         *dbRedis.DBAccessor
	name         string
	dbKey        string
	procUnitName string

	seasonSwitchWeekday           time.Weekday
	newSeasonWaitOldSeasonEndTime time.Duration

	lastDayVer  AtomicInt64           // 用于像巡演在大周期中还要有小每日周期的结算
	daySwitcher *StaticFixedDaySeason // set by outside
	// 可以重入, 处理过的不再处理
	OnDaySwitch func(curDayVer int64) (procOkCnt int, allOk bool)

	seasonBeginT AtomicInt64
	seasonEndT   AtomicInt64
	seasonState  AtomicInt32

	db_season_load                *dbRedis.DBScript
	db_season_last_day_ver_update *dbRedis.DBScript
	db_season_end_time_update     *dbRedis.DBScript
	db_season_state_update        *dbRedis.DBScript
	db_season_end                 *dbRedis.DBScript

	Open  func()
	Close func()
	// 可以重入, 全体可重新处理
	OnSeasonSwitch      func() (procCnt int, ok bool)
	OnSeasonSwitchBegin func()
	OnSeasonSwitchEnd   func()

	OnTimeStepCheck func()
}

func NewSeasonEvent(name string, dbKey string, procUnitName string, daySwitcher *StaticFixedDaySeason,
	seasonSwitchWeekday time.Weekday, newSeasonWaitOldSeasonEndTime time.Duration,
	openSeason func(), closeSeason func(), onSeasonSwitch func() (procCnt int, ok bool)) *SeasonEvent {
	return &SeasonEvent{
		name:         name,
		dbKey:        dbKey,
		procUnitName: procUnitName,
		daySwitcher:  daySwitcher,

		seasonSwitchWeekday:           seasonSwitchWeekday,
		newSeasonWaitOldSeasonEndTime: newSeasonWaitOldSeasonEndTime,

		Open:           openSeason,
		Close:          closeSeason,
		OnSeasonSwitch: onSeasonSwitch,
	}
}

func (self *SeasonEvent) AddDBScript(dbac *dbRedis.DBAccessor) {
	self.dbac = dbac

	self.db_season_load = dbRedis.NewDBScript("db_"+self.dbKey+"_season_load", 0, 1, strings.Join([]string{`
local seasonBeginT = 0
local seasonEndT = 0
local seasonState = 0
local lastDayVer = 0
local seasonDatas = redis.call("HMGET", "`, self.dbKey, `:season", "beginT", "endT", "state", "lastDayVer")
if seasonDatas[1] ~= false then
	seasonBeginT = tonumber(seasonDatas[1])
end
if seasonDatas[2] ~= false then
	seasonEndT = tonumber(seasonDatas[2])
end
if seasonDatas[3] ~= false then
	seasonState = tonumber(seasonDatas[3])
end
if seasonDatas[4] ~= false then
	lastDayVer = tonumber(seasonDatas[4])
end

-- make idx key for ranks
local ranksIdx = redis.call("GET", "`, self.dbKey, `:ranks:idx")
if ranksIdx == false then
	redis.call("SET", "`, self.dbKey, `:ranks:idx", 0)
end

return {1, seasonBeginT, seasonEndT, seasonState, lastDayVer}
`}, ""))

	self.db_season_last_day_ver_update = dbRedis.NewDBScript("db_"+self.dbKey+"_season_last_day_ver_update", 0, 8, strings.Join([]string{`
local lastDayVer = ARGV[1]
redis.call("HSET", "`, self.dbKey, `:season", "lastDayVer", lastDayVer)
return {1}
`}, ""))
	self.db_season_end_time_update = dbRedis.NewDBScript("db_"+self.dbKey+"_season_end_time_update", 0, 16, strings.Join([]string{`
local seasonBeginT = ARGV[1]
local seasonEndT = ARGV[2]
redis.call("HMSET", "`, self.dbKey, `:season", "beginT", seasonBeginT, "endT", seasonEndT)
return {1}
`}, ""))
	self.db_season_state_update = dbRedis.NewDBScript("db_"+self.dbKey+"_season_state_update", 0, 8, strings.Join([]string{`
local state = ARGV[1]
redis.call("HSET", "`, self.dbKey, `:season", "state", state)
return {1}
`}, ""))

	self.db_season_end = dbRedis.NewDBScript("db_"+self.dbKey+"_season_end", 0, 1, strings.Join([]string{`
local ranksIdx = redis.call("GET", "`, self.dbKey, `:ranks:idx")
if ranksIdx == "0" then
	ranksIdx = "1"
else
	ranksIdx = "0"
end
redis.call("SET", "`, self.dbKey, `:ranks:idx", ranksIdx)
local nextRanksKey = "`, self.dbKey, `:ranks:"..ranksIdx

redis.call("DEL", nextRanksKey)
return {1}
`}, ""))

	self.dbac.AddDBScript(self.db_season_load)
	self.dbac.AddDBScript(self.db_season_last_day_ver_update)
	self.dbac.AddDBScript(self.db_season_end_time_update)
	self.dbac.AddDBScript(self.db_season_state_update)
	self.dbac.AddDBScript(self.db_season_end)
}

func (self *SeasonEvent) Start() bool {
	ok := self.dbac.SyncOperateDB(self.name, self.db_season_load, make(chan byte, 1), func(datas []interface{}) {
		self.seasonBeginT.Set(dbRedis.GetInt64P(datas[0], "seasonBeginT"))
		self.seasonEndT.Set(dbRedis.GetInt64P(datas[1], "seasonEndT"))
		self.seasonState.Set(int32(dbRedis.GetInt64P(datas[2], "seasonState")))
		self.lastDayVer.Set(dbRedis.GetInt64P(datas[3], "lastDayVer"))
	})
	if !ok {
		return false
	}

	// set open or close before Start(update)
	switch self.seasonState.Get() {
	case check_state_normal:
		self.Open()
	case check_state_going:
		log.Info(self.name, log.I, "[update to new season], continue from last interrupted season switch")
		self.Close()
	}

	go self.checkNewSeason()

	return true
}

func (self *SeasonEvent) checkNewSeason() {
	signal := make(chan byte, 1)

	changeDayVer := func(dayVer int64) bool {
		return self.dbac.SyncOperateDB(self.name, self.db_season_last_day_ver_update, signal, func(datas []interface{}) {
			self.lastDayVer.Set(dayVer)
		}, dayVer)
	}

	changeState := func(state int32) bool {
		return self.dbac.SyncOperateDB(self.name, self.db_season_state_update, signal, func(datas []interface{}) {
			self.seasonState.Set(state)
		}, state)
	}

	changeSeasonEndT := func(beginT int64, endT int64) bool {
		return self.dbac.SyncOperateDB(self.name, self.db_season_end_time_update, signal, func(datas []interface{}) {
			self.seasonBeginT.Set(beginT)
			self.seasonEndT.Set(endT)
		}, beginT, endT)
	}

	/* 这里的5分钟, 是为了逐步推进赛季切换时间点的一个合理的步调, 不直接用timer24小时这样的大步调,
	为的是能够在出错的时候, 有一个重试的缓冲时间, 虽然有误差, 不能精确地切换,
	但能保证一旦出错, 在修复错误的时间点之后5分钟内能完成切换
	同时这里也考虑了一场打歌的时间在3分钟以内, 也有配合这个时间间隔的意思*/
	UpdateProc(time.Minute*5, true, func() bool {
		if self.OnTimeStepCheck != nil {
			self.OnTimeStepCheck()
		}

		switch self.seasonState.Get() {
		case check_state_normal:
			//保证先触发每日, 然后再是赛季切换, 不然先切换赛季就会抹掉最后一次的信息
			lastDayVer := self.lastDayVer.Get()
			curDayVer := self.daySwitcher.GetSeasonVer()
			if lastDayVer == 0 {
				// use current day ver to init lastDayVer
				changeDayVer(curDayVer)
			} else if lastDayVer != curDayVer {
				procOkCnt := 0
				allOk := true
				if self.OnDaySwitch != nil {
					procOkCnt, allOk = self.OnDaySwitch(curDayVer)
					log.Info(self.name, log.I, "[proc day switch in season], curDayVer: %d, proccessed %s number:%d, allOk:%t", curDayVer, self.procUnitName, procOkCnt, allOk)
					if !allOk {
						log.Error(self.name, "[proc day switch in season], not all ok, wait to next retry")
					}
				}
				if allOk {
					changeDayVer(curDayVer)
				} else {
					return true
				}
			}

			now := time.Now()
			nowT := now.UnixNano()

			seasonEndT := self.seasonEndT.Get()
			if seasonEndT == 0 {
				seasonEndT, _ = GetCheckpointTByWeek(seasonEndT, now, self.seasonSwitchWeekday)

				// season是以天为单位操作的, 所以若间隔小于一天, 可能某些细节会跟预期有出入
				// 比如这里, 若间隔小于一天, 那么可能endt已经小于now了, 这个时候其实应该直接触发切换, 而不是再等待5分钟
				if !changeSeasonEndT(nowT, seasonEndT) {
					return true // wait for next try
				}
			} else if seasonEndT < nowT {
				log.Info(self.name, log.I, "[update to new season], close ......")
				self.Close()

				seasonEndT, _ = GetCheckpointTByWeek(seasonEndT, now, self.seasonSwitchWeekday)

				if !changeSeasonEndT(nowT, seasonEndT) {
					return true // wait for next try
				}

				if !changeState(check_state_going) {
					return true // wait for next try
				}

				// waiting for old season procs end
				log.Info(self.name, log.I, "[update to new season], waiting for all old season procs end......[%s]", self.newSeasonWaitOldSeasonEndTime)

				time.Sleep(self.newSeasonWaitOldSeasonEndTime)
			}

		case check_state_going:
			log.Info(self.name, log.I, "[update to new season], proccess all %ss......", self.procUnitName)

			if self.OnSeasonSwitchBegin != nil {
				self.OnSeasonSwitchBegin()
			}
			defer func() {
				if self.OnSeasonSwitchEnd != nil {
					self.OnSeasonSwitchEnd()
				}
			}()

			procCnt, ok := self.OnSeasonSwitch()
			if ok {
				UpdateProc(time.Second*30, true, func() bool {
					log.Info(self.name, log.I, "[update to new season], proccessed %s number:%d, switch to new season......", self.procUnitName, procCnt)
					// if fail, will retry until success
					ok1 := self.dbac.SyncOperateDB(self.name, self.db_season_end, signal, func(datas []interface{}) {})
					ok2 := changeState(check_state_normal)
					return !(ok1 && ok2)
				})

				self.Open()
			}
		}

		return true
	})
}

func (self *SeasonEvent) GetCurSeasonBeginEndTS() (beginTS int64, endTS int64) {
	beginTS = self.seasonBeginT.Get()
	beginTS /= int64(time.Second)
	endTS = self.seasonEndT.Get()
	endTS /= int64(time.Second)
	return
}
